package com.dbflow5.transaction;

import com.dbflow5.TriFunction;
import com.dbflow5.database.DatabaseWrapper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;

//typealias ProcessFunction<T> = (T, DatabaseWrapper) -> Unit

/**
 * Listener for providing callbacks as models are processed in this [ITransaction].
 *
 * Called when model has been operated on.
 *
 * @param current       The current index of items processed.
 * @param total         The total number of items to process.
 * @param modifiedModel The model previously modified.
 * @param <TModel> The model class.
</TModel> */
//typealias OnModelProcessListener<TModel> = (current: Long, total: Long, modifiedModel: TModel) -> Unit

/**
 * Description: Allows you to process a single or [List] of models in a transaction. You
 * can operate on a set of [Model] to [Model.save], [Model.update], etc.
 */
public class ProcessModelTransaction<TModel> implements ITransaction<Void> {

    private List<TModel> models = new ArrayList<>();
    private TriFunction<Long, Long, TModel, Void> processListener = null;
    private final ProcessModel<TModel> processModel;
    private final boolean runProcessListenerOnSameThread;

    /**
     * Description: Simple interface for acting on a model in a Transaction or list of [Model]
     */
    public interface ProcessModel<TModel> {

        /**
         * Called when processing models
         *
         * @param model   The model to process
         * @param wrapper wrapper
         */
        void processModel(TModel model, DatabaseWrapper wrapper);
    }

    ProcessModelTransaction(Builder<TModel> builder) {
        processListener = builder.processListener;
        models = builder.models;
        processModel = builder.processModel;
        runProcessListenerOnSameThread = builder.runProcessListenerOnSameThread;
    }

    @Override
    public Void execute(DatabaseWrapper databaseWrapper) {
        int size = models.size();
        for (int index=0;index < size ; index++) {
            TModel model = models.get(index);
            processModel.processModel(model, databaseWrapper);

            if (processListener != null) {
                if (runProcessListenerOnSameThread) {
                    processListener.apply((long) index, (long)size, model);
                } else {
                    int finalIndex = index;
                    Transaction.transactionHandler.postTask(() -> processListener.apply((long) finalIndex, (long)size, model));
                }
            }
        }
        return null;
    }

    /**
     * Makes it easy to build a [ProcessModelTransaction].
     *
     * @param <TModel>
     */
    public static class Builder<TModel> {

        ProcessModel<TModel> processModel;
        TriFunction<Long, Long, TModel, Void> processListener = null;
        List<TModel> models = new ArrayList<>();
        boolean runProcessListenerOnSameThread = false;


        public Builder(ProcessModel<TModel> processModel) {
            this.processModel = processModel;
        }

        /**
         * @param models       The models to process. This constructor creates a new [ArrayList]
         * from the [Collection] passed.
         * @param processModel The method call interface.
         */
        public Builder(Collection<TModel> models, ProcessModel<TModel> processModel) {
            this.processModel = processModel;
            this.models = new ArrayList<>(models);
        }

        public Builder<TModel> add(TModel model) {
            models.add(model);
            return this;
        }

        /**
         * Adds all specified models to the [ArrayList].
         * @param models models
         * @return Builder
         */
        @SafeVarargs
        public final Builder<TModel> addAll(TModel... models) {
            this.models.addAll(Arrays.asList(models));
            return this;
        }

        /**
         * Adds a [Collection] of [Model] to the existing [ArrayList].
         * @param models models
         * @return Builder
         */
        public Builder<TModel> addAll(Collection<TModel> models) {
            if (models != null) {
                this.models.addAll(models);
            }
            return this;
        }

        /**
         * TModel Builder
         * @param processListener Allows you to listen for when models are processed to update UI,
         * this is called on the UI thread.
         * @return Builder
         */
        public Builder<TModel> processListener(TriFunction<Long, Long, TModel, Void> processListener) {
            this.processListener = processListener;
            return this;
        }

        /**
         * TModel Builder
         * @param runProcessListenerOnSameThread Default is false. If true we return callback
         * on same calling thread, if false we push the callback
         * to the UI thread.
         * @return Builder
         */
        public Builder<TModel> runProcessListenerOnSameThread(boolean runProcessListenerOnSameThread) {
            this.runProcessListenerOnSameThread = runProcessListenerOnSameThread;
            return this;
        }

        /**
         * A new [ProcessModelTransaction]
         * @return A new [ProcessModelTransaction]. Subsequent calls to this method produce
         * new instances.
         */
        public ProcessModelTransaction<TModel> build() {
            return new ProcessModelTransaction<>(this);
        }
    }

    public static <T> ProcessModelTransaction.ProcessModel<T> processModel(BiFunction<T, DatabaseWrapper, Void> function) {
        return function::apply;
    }

    public static <T> ProcessModelTransaction.Builder<T> processTransaction(Collection<T> models, BiFunction<T, DatabaseWrapper, Void> processFunction){
        ProcessModelTransaction.ProcessModel<T> processModel = processModel((model, databaseWrapper) -> {
            processFunction.apply(model, databaseWrapper);
            return null;
        });
        ProcessModelTransaction.Builder<T> builder = new ProcessModelTransaction.Builder<>(processModel);
        builder.addAll(models);
        return builder;
    }
}
